<template>
  <component class="DomMoveScale"
             :is="tag"
             ref="elRef"
             @click.stop="$emit('click')"
             @touchstart.prevent="onTouchstart"
             @mousedown.prevent="onMousedown"
             @wheel.prevent="onWheel">
    <slot v-bind="{ transform: computedTransform }"></slot>
  </component>
</template>

<script>
export default {
  name: 'DomMoveScale',
  props: {
    tag: {
      type: String,
      default: 'div'
    },
    transform: {
      type: Object,
      default: () => ({})
    },
    minScale: {
      type: Number,
      default: 0.2
    },
    maxScale: {
      type: Number,
      default: 2
    }
  },
  emits: ['update:transform', 'click'],
  data() {
    return {
      origin: {
        translateX: 0,
        translateY: 0,
        scale: 0,
        x: 0,
        y: 0,
        rectX: 0,
        rectY: 0,
        centerX: 0,
        centerY: 0,
        offsetX: 0,
        offsetY: 0,
        length: 0
      },
    }
  },
  computed: {
    computedTransform() {
      return Object.entries(this.transform)
        .map(([k, v]) => `${k}(${v}${k.startsWith('translate') ? 'px' : ''})`)
        .join(' ')
    }
  },
  mounted() {
    this.initializeTransform()
  },
  methods: {
    initializeTransform() {
      const props = this.transform
      if ([props.translateX, props.translateY, props.scale].includes(undefined)) {
        const newTransform = { ...props }
        if (props.translateX === undefined) newTransform.translateX = 0
        if (props.translateY === undefined) newTransform.translateY = 0
        if (props.scale === undefined) {
          newTransform.scale = this.minScale <= 1 && 1 <= this.maxScale ? 1 : (this.minScale + this.maxScale) / 2
        }
        this.$emit('update:transform', newTransform)
      }
    },

    setCentre(touches) {
      this.origin = {
        ...this.origin,
        translateX: this.transform.translateX,
        translateY: this.transform.translateY,
        scale: this.transform.scale,
        ...this.getRectValues(),
        centerX: (touches[0].clientX + touches[1].clientX) / 2,
        centerY: (touches[0].clientY + touches[1].clientY) / 2,
        offsetX: (this.origin.centerX - this.origin.rectX - this.origin.translateX),
        offsetY: (this.origin.centerY - this.origin.rectY - this.origin.translateY),
        length: Math.hypot(
          touches[0].clientX - touches[1].clientX,
          touches[0].clientY - touches[1].clientY
        )
      }
    },

    getRectValues() {
      const rect = this.$refs.elRef.getBoundingClientRect()
      return {
        rectX: rect.x,
        rectY: rect.y
      }
    },

    onWheel(e) {
      if (!e.deltaY) return
      const oldScale = this.transform.scale
      let newScale = oldScale

      if (e.deltaY > 0) {
        newScale = Math.max(this.minScale, oldScale - 0.1)
      } else {
        newScale = Math.min(this.maxScale, oldScale + 0.1)
      }

      const rect = this.$refs.elRef.getBoundingClientRect()
      const newTransform = {
        ...this.transform,
        scale: newScale,
        translateX: this.transform.translateX + (e.clientX - rect.x - this.transform.translateX) * (1 - newScale / oldScale),
        translateY: this.transform.translateY + (e.clientY - rect.y - this.transform.translateY) * (1 - newScale / oldScale)
      }

      this.$emit('update:transform', newTransform)
    },

    onMousedown(e) {
      this.origin = {
        ...this.origin,
        translateX: this.transform.translateX,
        translateY: this.transform.translateY,
        x: e.clientX,
        y: e.clientY
      }

      const onMove = e => {
        e.preventDefault()
        const newTransform = {
          ...this.transform,
          translateX: e.clientX - this.origin.x + this.origin.translateX,
          translateY: e.clientY - this.origin.y + this.origin.translateY
        }
        this.$emit('update:transform', newTransform)
      }

      const onEnd = () => {
        this.$refs.elRef.removeEventListener('mousemove', onMove)
        this.$refs.elRef.removeEventListener('mouseup', onEnd)
        this.$refs.elRef.style.removeProperty('cursor')
      }

      this.$refs.elRef.addEventListener('mousemove', onMove)
      this.$refs.elRef.addEventListener('mouseup', onEnd)
      this.$refs.elRef.style.cursor = 'move'
    },

    onTouchstart(e) {
      switch (e.touches.length) {
        case 1:
          this.handleSingleTouch(e)
          break
        case 2:
          this.handleDoubleTouch(e)
          break
      }
    },

    handleSingleTouch(e) {
      document.body.style.overflow = 'hidden'
      this.origin = {
        ...this.origin,
        translateX: this.transform.translateX,
        translateY: this.transform.translateY,
        x: e.touches[0].clientX,
        y: e.touches[0].clientY
      }

      const onMove = e => {
        e.preventDefault()
        if (e.touches.length !== 1) return
        const newTransform = {
          ...this.transform,
          translateX: e.touches[0].clientX - this.origin.x + this.origin.translateX,
          translateY: e.touches[0].clientY - this.origin.y + this.origin.translateY
        }
        this.$emit('update:transform', newTransform)
      }

      const onEnd = e => {
        if (e.touches.length) return
        this.$refs.elRef.removeEventListener('touchmove', onMove)
        this.$refs.elRef.removeEventListener('touchend', onEnd)
        document.body.style.removeProperty('overflow')
      }

      this.$refs.elRef.addEventListener('touchmove', onMove)
      this.$refs.elRef.addEventListener('touchend', onEnd)
    },

    handleDoubleTouch(e) {
      e.preventDefault()
      this.setCentre(e.touches)

      const onMove = e => {
        if (e.touches.length !== 2) return
        const scale = Math.max(
          this.minScale,
          Math.min(
            this.maxScale,
            this.origin.scale *
            (Math.hypot(
              e.touches[0].clientX - e.touches[1].clientX,
              e.touches[0].clientY - e.touches[1].clientY
            ) / this.origin.length)
          )
        )

        const center = {
          x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
          y: (e.touches[0].clientY + e.touches[1].clientY) / 2
        }

        const rect = this.getRectValues()
        const newTransform = {
          ...this.transform,
          scale,
          translateX:
            this.origin.offsetX * (1 - scale / this.origin.scale) +
            this.origin.translateX +
            (center.x - this.origin.centerX) -
            (rect.rectX - this.origin.rectX),
          translateY:
            this.origin.offsetY * (1 - scale / this.origin.scale) +
            this.origin.translateY +
            (center.y - this.origin.centerY) -
            (rect.rectY - this.origin.rectY)
        }

        this.$emit('update:transform', newTransform)
      }

      const onEnd = e => {
        if (e.touches.length === 2) {
          this.setCentre(e.touches)
          return
        }
        if (e.touches.length === 1) {
          this.origin = {
            ...this.origin,
            translateX: this.transform.translateX,
            translateY: this.transform.translateY,
            x: e.touches[0].clientX,
            y: e.touches[0].clientY
          }
        }
        this.$refs.elRef.removeEventListener('touchmove', onMove)
        this.$refs.elRef.removeEventListener('touchend', onEnd)
      }

      this.$refs.elRef.addEventListener('touchmove', onMove)
      this.$refs.elRef.addEventListener('touchend', onEnd)
    }
  }
}
</script>

<style scoped>
:slotted(*) {
  transform-origin: left top;
}
</style>
